#include "session.h"

#include "tracer.h"
#include "config-fnt.h"
#include "graph.h"
#include "viewer.h"
#include "compute.thread.h"
#include "load.thread.h"
#include "listmodel.h"
#include "options.h"
#include "stream.h"
#include "misc.h"
#include "dialogs.h"
#include "options.dlg.h"
#include "plugins/plugin.h"
#include "plugin.manager.h"
#include "command.h"
#include "graph.priv.h"
#include "annot.tree.h"

#include <QtWidgets/QtWidgets>
#include <memory>

#include "session.options.h"

struct SessionPriv {
	Session* session;
	Viewer* viewer;
	SessionState state;

	std::array<QAction*, SESSION_ACTION_NUM> actions;
	QMenu* menuMisc;
	QMenu* popupViewer;
	QMenu* popupList;

	QWidget* sidePanel;
	QTreeView* listView;
	QSlider* sliderCur;
	QPushButton* curLeft;
	QPushButton* curRight;
	QSlider* sliderTgt;
	QPushButton* tgtLeft;
	QPushButton* tgtRight;

	const AnnotTree* annotTree;
	AnnotModel* annotModel;
	QListWidget* annotHeader;
	QComboBox* annotList;
	QTreeView* annotView;
	Cube annotCube;
	int annot_map;

	QStatusBar* statusBar;
	QLabel* statLabel;
	QProgressBar* progr;

	// Session
	LoadThreadInput image_input;
	QString file;
	int64_t verFile; /* -2 unreachable, -1, non-existant, 0... version */
	int64_t verSwap;
	int64_t ver;
	LoadThread* loadThread;
	ComputeThread* computeThread;

	// Tracing
	Graph graph;
	ListModel* listModel;
	Path path;
	bool pathConnected;
	std::list<Command*> undoList;
	std::list<Command*> redoList;

	// SessionState and settings
	std::array<Cube, 9> cubes;
	std::array<std::pair<double, double>, 9> xfuncs;
	std::array<int, 9> cube_maps;
	std::array<double, 6> bbox;
	double mvol;
	Position curPos;
	Position tgtPos;
	double sight; // 100
	Options options;
	bool loadDataAuto;
	int curRow;

	QVector<QString> channels;

	QTimer* timer_5min;
	int64_t timer_check;

	SessionPriv(Session* s):
		session{s}, viewer{nullptr}, state{SessionState::Invalid},
		listView{nullptr},
		sliderCur{nullptr}, curLeft{nullptr}, curRight{nullptr},
		sliderTgt{nullptr}, tgtLeft{nullptr}, tgtRight{nullptr},
		annotTree{nullptr}, annotModel{nullptr}, annotView{nullptr}, annotCube{}, annot_map{-1},
		statLabel{nullptr}, progr{nullptr},
		image_input{}, file{},
		verFile{-1}, verSwap{-1}, ver{-1},
		loadThread{nullptr}, computeThread{nullptr},
		graph{}, listModel{nullptr},
		path{},
		undoList{}, redoList{},
		cubes{Cube{},}, cube_maps{0,},
		bbox{qInf(), -qInf(), qInf(), -qInf(), qInf(), -qInf()},
		mvol{qInf()},
		curPos{}, tgtPos{},
		options{}, loadDataAuto{true}, curRow{-1}, timer_5min{nullptr}
	{
		viewer=new Viewer{session};
		session->setCentralWidget(viewer);
		annot_map=2;
		timer_5min=new QTimer{session};
		QObject::connect(timer_5min, &QTimer::timeout, session, &Session::timeoutFiveMinute);
	}
	SessionPriv(Session* s, const LoadThreadInput& input):
		SessionPriv{s}
	{
		ver=0;
		verFile=-1;
		verSwap=-1;
		setFile(QString{});

		graph=GraphPriv::alloc();
		image_input=input;
		options.setXform("image.xform", image_input.direction, image_input.origin);

		Tracer::instance()->logMessage("STAT: ", "SESSION_NEW");
	}
	SessionPriv(Session* s, const QString& file):
		SessionPriv{s}
	{
		verFile=0;
		ver=0;
		verSwap=-1;
		setFile(file);

		graph=GraphPriv::alloc();
		load(file, false);

		Tracer::instance()->logMessage("STAT: ", "SESSION_OPEN");
		dumpGraphDetail();
	}
	SessionPriv(Session* s, const QString& file, const QString& swap):
		SessionPriv{s}
	{
		verFile=-2;
		verSwap=0;
		ver=0;
		setFile(file);

		graph=GraphPriv::alloc();
		load(swap, true);

		Tracer::instance()->logMessage("STAT: ", "SESSION_OPEN_SWAP");
		dumpGraphDetail();
	}
	void postConstruct() {
		loadThread=new LoadThread{session, image_input};
		computeThread=new ComputeThread{session};
		listModel=new ListModel{session, graph};
		listView->setModel(listModel);
		QObject::connect(listView->selectionModel(), &QItemSelectionModel::currentRowChanged, session, &Session::currentChanged);
		listView->setEnabled(true);
		QObject::connect(listView->selectionModel(), &QItemSelectionModel::selectionChanged, session, &Session::selectionChanged);

		QObject::connect(loadThread, &LoadThread::threadWarning, session, &Session::loadThreadWarning);
		QObject::connect(loadThread, &LoadThread::threadError, session, &Session::loadThreadError);
		QObject::connect(loadThread, &LoadThread::sourcesReady, session, &Session::sourcesReady);
		QObject::connect(loadThread, &LoadThread::cubeFinished, session, &Session::cubeFinished);
		QObject::connect(loadThread, &LoadThread::updateProgress, session, &Session::updateProgress);

		QObject::connect(computeThread, &ComputeThread::computeFinished, session, &Session::computeFinished);
		QObject::connect(computeThread, &ComputeThread::showProgress, session, &Session::showComputeProgress);
		QObject::connect(computeThread, &ComputeThread::threadWarning, session, &Session::computeThreadWarning);
		QObject::connect(computeThread, &ComputeThread::threadError, session, &Session::computeThreadError);

		timer_check=ver;
		timer_5min->setSingleShot(false);
		timer_5min->setInterval(5*60*1000);
		timer_5min->start();

		computeThread->start();
		loadThread->start();
		setState(SessionState::LoadingCatalog, "Loading catalog", true);
	}

	void setupSidePanel();
	void setupAnnotPanel();
	void setupStatusBar();
	void setupMenuBarAndToolBar();
	void setupUi();
	void setupActions();
	void setupMenus();

	void dumpGraphDetail() {
		std::vector<double> lengths(graph.trees().size()+1, 0.0);
		std::vector<int64_t> segments(graph.trees().size()+1, 0);
		std::vector<int> branches(graph.trees().size()+1, 0);

		for(auto e: graph.edges()) {
			double len=0;
			for(size_t i=1; i<e.length(); i++) {
				len+=e.points()[i].distTo(e.points()[i-1]);
			}
			if(e.tree()) {
				lengths[e.tree().index()]+=len;
				segments[e.tree().index()]+=e.points().size()-1;
			} else {
				lengths[graph.trees().size()]+=len;
				segments[graph.trees().size()]+=e.points().size()-1;
			}
		}
		for(auto v: graph.vertices()) {
			if(!v.parentEdge())
				continue;
			if(v.neighbors().size()!=1)
				continue;
			if(v.tree()) {
				branches[v.tree().index()]+=1;
			} else {
				branches[graph.trees().size()]+=1;
			}
		}
		for(size_t i=0; i<graph.trees().size()+1; i++) {
			Tracer::instance()->logMessage("STAT: DETAIL: ", i, ": ", branches[i], "b, ", segments[i], "s, ", lengths[i], "um");
		}
		timer_check=ver;
	}

	~SessionPriv() {
		if(computeThread) {
			if(computeThread->isRunning()) {
				computeThread->stop();
				if(!computeThread->wait())
					computeThread->terminate();
			}
			delete computeThread;
		}
		if(loadThread) {
			if(loadThread->isRunning()) {
				loadThread->stop();
				if(!loadThread->wait()) {
					loadThread->terminate();
				}
			}
			delete loadThread;
		}
		if(graph) {
			for(auto e: graph.edges()) {
				viewer->recycleEdge(EdgePriv::get(e));
			}
			GraphPriv::free(graph);
		}

		//recentMenu.close(file);

		for(auto p: undoList)
			delete p;
		for(auto p: redoList)
			delete p;
	}

	void updateSlider(bool c, const Position& p) {
		auto slider=c ? sliderCur : sliderTgt;
		auto lft=c ? curLeft : tgtLeft;
		auto rgt=c ? curRight : tgtRight;
		if(p.edge) {
			bool prev=slider->blockSignals(true);
			slider->setMinimum(0);
			slider->setMaximum(p.edge.points().size()-1);
			slider->setValue(p.index);
			lft->setEnabled(p.index!=0);
			rgt->setEnabled(p.index!=p.edge.points().size()-1);
			slider->setEnabled(true);
			slider->blockSignals(prev);
		} else {
			slider->setEnabled(false);
			lft->setEnabled(false);
			rgt->setEnabled(false);
		}
	}

	bool saveSession() {
		switch(state) {
			case SessionState::Invalid:
				return false;
			case SessionState::LoadingCatalog:
			case SessionState::Readonly:
			case SessionState::Ready:
			case SessionState::Computing:
			case SessionState::LoadingCubes:
				break;
		}
		if(ver==verFile) {
			showMessage("Save session", "Nothing new to save", session);
			return false;
		}
		if(file.isEmpty()) {
			return saveSessionAs();
		}
		try {
			saveOptions();
			save(file, false);
		} catch(const std::exception& e) {
			showWarning("Failed to save session", e.what(), session);
			updateActions();
			return false;
		}
		updateActions();
		Tracer::instance()->logMessage("STAT: ", "SESSION_SAVE");
		dumpGraphDetail();
		return true;
	}
	bool saveSessionAs() {
		switch(state) {
			case SessionState::Invalid:
				return false;
			case SessionState::LoadingCatalog:
			case SessionState::Readonly:
			case SessionState::Ready:
			case SessionState::Computing:
			case SessionState::LoadingCubes:
				break;
		}

		QFileDialog dlg(session, "Please choose a session file to save.", "", "");
		dlg.setAcceptMode(QFileDialog::AcceptSave);
		dlg.setDefaultSuffix("fnt");
		dlg.setFileMode(QFileDialog::AnyFile);
		dlg.setNameFilter("FNT Session file (*.fnt *.fntz)");
		if(dlg.exec()!=QDialog::Accepted)
			return false;

		try {
			saveOptions();
			save(dlg.selectedFiles()[0], false);
		} catch(const std::exception& e) {
			showWarning("Failed to save session", e.what(), session);
			updateActions();
			return false;
		}
		updateActions();
		Tracer::instance()->logMessage("STAT: ", "SESSION_SAVE");
		dumpGraphDetail();
		return true;
	}

	bool readGraph(QObject* sender) {
		switch(state) {
			case SessionState::Invalid:
			case SessionState::LoadingCatalog:
			case SessionState::Readonly:
				return false;
			case SessionState::Ready:
			case SessionState::Computing:
			case SessionState::LoadingCubes:
				break;
		}

		auto a=qobject_cast<QAction*>(sender);
		if(!a)
			return false;
		bool ok;
		auto i=a->data().toInt(&ok);
		if(!ok)
			return false;

		try {
			std::unique_ptr<GraphReader> grd{PluginManager::instance()->graphReaders()[i].first->creater()};
			for(int i=0; i<4; i++) {
				CubeRef cuberef;
				auto& cube=i<3?cubes[3*i]:annotCube;

				cuberef.data=nullptr;
				if(cube.data) {
					switch(cube.data.type()) {
						case CubeType::U8:
							cuberef.dataU8=reinterpret_cast<const uint8_t*>(cube.data.data());
							cuberef.is16bit=false;
							break;
						case CubeType::U16:
							cuberef.dataU16=reinterpret_cast<const uint16_t*>(cube.data.data());
							cuberef.is16bit=true;
							break;
						default:
							cuberef.data=nullptr;
					}
				}
				if(cuberef.data) {
					cuberef.width=cube.data.width();
					cuberef.height=cube.data.height();
					cuberef.depth=cube.data.depth();

					cuberef.widthAdj=cube.data.widthAdj();
					cuberef.heightAdj=cube.data.heightAdj();
					cuberef.depthAdj=cube.data.depthAdj();

					cuberef.xres=cube.xform.resolution[0];
					cuberef.yres=cube.xform.resolution[1];
					cuberef.zres=cube.xform.resolution[2];

					for(int i=0; i<3; i++)
						cuberef.origin[i]=cube.xform.origin[i]+cube.xform.direction[i]*cube.data.cubeId().x0+cube.xform.direction[i+3]*cube.data.cubeId().y0+cube.xform.direction[i+6]*cube.data.cubeId().z0;
					for(int i=0; i<9; i++)
						cuberef.dir[i]=cube.xform.direction[i];
					for(int i=0; i<9; i++)
						cuberef.rdir[i]=cube.xform.direction_inv[i];
				}
				if(i<3) {
					grd->setupCube(i, cuberef, xfuncs[3*i]);
				} else {
					grd->setupCubeAnnot(cuberef, annotTree?&annotTree->annots():nullptr);
				}
			}
			grd->read(graph, session);
		} catch(const std::exception& e) {
			showWarning("Failed to show session information", e.what(), session);
			return false;
		}
		return true;
	}

	bool closeSessionImp() {
		switch(state) {
			case SessionState::Invalid:
				return false;
			case SessionState::LoadingCatalog:
			case SessionState::Computing:
			case SessionState::LoadingCubes:
				showMessage("Close session", "Please cancel running operations first.", session);
				return false;
			case SessionState::Readonly:
			case SessionState::Ready:
				break;
		}

		if(ver!=verFile) {
			QMessageBox mbox(QMessageBox::Question, "Save Session?", "Do you want to save changes to the current session?", QMessageBox::Save|QMessageBox::Cancel|QMessageBox::Discard, session);
			mbox.setDefaultButton(QMessageBox::Save);
			auto res=mbox.exec();
			if(res==QMessageBox::Cancel) {
				return false;
			}
			if(res==QMessageBox::Save) {
				if(!saveSession()) {
					return false;
				}
			}
		}

		setState(SessionState::Invalid, "Ready to be destroyed.", false);

		Tracer::instance()->logMessage("STAT: ", "SESSION_CLOSE");
		dumpGraphDetail();
		return true;
	}

	void sourcesReady() {
		loadThread->getBoundingBox(&bbox);
		loadThread->getMinVoxelSize(&mvol);

		channels=loadThread->sources();
		SessionChannelOptions::instance()->config(this, &options);
		SessionLoadingOptions::instance()->config(this, &options);

		if(!curPos.point.valid()) {
			if(bbox[0]>bbox[1]) {
				curPos.point.x(0);
			} else {
				curPos.point.x((bbox[0]+bbox[1])/2);
			}
			if(bbox[2]>bbox[3]) {
				curPos.point.y(0);
			} else {
				curPos.point.y((bbox[2]+bbox[3])/2);
			}
			if(bbox[4]>bbox[5]) {
				curPos.point.z(0);
			} else {
				curPos.point.z((bbox[4]+bbox[5])/2);
			}
			curPos.point.m=0;
		}

		updateSlider(false, tgtPos);
		updateSlider(true, curPos);

		{
			if(!options.getInt("channel.map.annot", &annot_map))
				annot_map=0;
			auto b=annotList->blockSignals(true);
			annotList->clear();
			for(int j=0; j<channels.size(); j++) {
				annotList->addItem(channels[j]);
			}
			annotList->setCurrentIndex(annot_map);
			annotList->blockSignals(b);
			annotList->setEnabled(true);
		}

		viewer->setSession(&options, graph, cubes, xfuncs, bbox, mvol);
		viewer->updatePosition(tgtPos, false);
		viewer->updatePosition(curPos, true);
		viewer->update();

		if(state==SessionState::LoadingCatalog)
			setState(SessionState::Ready, "Ready.", false);
		if(loadDataAuto)
			startDownloadCube();
	}
	void load(const QString& fn, bool swap) {
		InputStream fs;
		if(!fs.open(fn.toLocal8Bit().constData()))
			throwError("Failed to open file");
		fs.precision(13);
		fs.setf(std::ios::scientific);

		std::string line;
		std::getline(fs, line);
		if(!fs)
			throwError("Failed to read magic header");
		bool gzipped=false;
		if(line==FNTZ_MAGIC) {
			gzipped=true;
		} else if(line!=FNT_MAGIC) {
			throwError("Wrong magic: ", line);
		}

		std::getline(fs, line);
		if(!fs)
			throwError("Failed to read image location line");
		image_input.location=QString::fromStdString(line);

		std::getline(fs, line);
		if(!fs)
			throwError("Failed to read line");
		while(line!="BEGIN_TRACING_DATA") {
			options.putLine(QString::fromStdString(line));
			std::getline(fs, line);
			if(!fs)
				throwError("Failed to read line");
		}

		if(gzipped) {
			if(!fs.pushGzip())
				throwError("Failed to start decompressing");
		}
		auto gp=GraphPriv::get(graph);
		gp->load(fs);
		if(gzipped) {
			if(!fs.pop())
				throwError("Failed to finish decompressing");
		}
		if(!fs.close())
			throwError("Failed to close file.");

		gp->updateModel();
		if(!options.getXform("image.xform", &image_input.direction, &image_input.origin)) {
			for(int i=0; i<3; i++)
				for(int j=0; j<3; j++)
					image_input.direction[i+3*j]=i==j?1:0;
			for(int i=0; i<3; i++)
				image_input.origin[i]=0;
		}

		for(auto e: graph.edges()) {
			auto& p0=e.points()[0];
			updateBoundingBox(p0.x(), p0.y(), p0.z());
			auto& p1=e.points()[e.points().size()-1];
			updateBoundingBox(p1.x(), p1.y(), p1.z());
		}
	}
	void save(const QString& fn, bool swap) {
		printMessage("Saving ", fn, "...");
		OutputStream fs;
		if(!fs.open(fn.toLocal8Bit().constData()))
			throwError("Cannot open file");
		fs.precision(13);
		fs.setf(std::ios::scientific);

		bool gzipped=false;
		if(swap || fn.endsWith(".fntz") || fn.endsWith(".fnt.gz"))
			gzipped=true;

		if(gzipped)
			fs<<FNTZ_MAGIC<<'\n';
		else
			fs<<FNT_MAGIC<<'\n';
		if(!fs)
			throwError("Failed to write magic");

		fs<<image_input.location<<'\n';
		if(!fs)
			throwError("Failed to write image location");

		for(auto& line: options.getLines()) {
			fs<<line<<'\n';
			if(!fs)
				throwError("Failed to write option line");
		}

		fs<<"BEGIN_TRACING_DATA"<<'\n';
		if(!fs)
			throwError("Failed to write data start mark");

		if(gzipped) {
			if(!fs.pushGzip())
				throwError("Failed to start compressing");
		}
		GraphPriv::get(graph)->save(fs);
		if(gzipped) {
			if(!fs.pop())
				throwError("Failed to finish compressing");
		}
		if(!fs.close())
			throwError("Failed to close file");

		if(swap) {
			verSwap=ver;
		} else {
			//recentMenu.saveas(file, fn);
			verFile=ver;
			setFile(fn);
		}
	}
	void updateActions() {
		switch(state) {
			case SessionState::Invalid:
				actions[SESSION_SAVE]->setEnabled(false);
				actions[SESSION_SAVE_AS]->setEnabled(false);
				menuMisc->setEnabled(false);
				actions[SESSION_CLOSE]->setEnabled(false);
				actions[EDIT_UNDO]->setEnabled(false);
				actions[EDIT_REDO]->setEnabled(false);
				actions[EDIT_GOTO_POS]->setEnabled(false);
				actions[EDIT_GOTO_TARGET]->setEnabled(false);
				actions[EDIT_PICK_CURRENT]->setEnabled(false);
				actions[EDIT_LOAD_DATA]->setEnabled(false);
				actions[EDIT_LOAD_DATA_AUTO]->setEnabled(false);
				actions[EDIT_CHANGE_TYPE]->setEnabled(false);
				actions[EDIT_CHANGE_MARK]->setEnabled(false);
				actions[EDIT_CHANGE_SIZE]->setEnabled(false);
				actions[EDIT_NEW_NEURON]->setEnabled(false);
				actions[EDIT_RENAME_NEURON]->setEnabled(false);
				actions[EDIT_DEL_NEURONS]->setEnabled(false);
				actions[EDIT_PURGE_NEURONS]->setEnabled(false);
				actions[EDIT_IMPORT_FNT]->setEnabled(false);
				actions[EDIT_IMPORT_SWC]->setEnabled(false);
				actions[EDIT_ABORT]->setEnabled(false);
				actions[TRACING_FIND]->setEnabled(false);
				actions[TRACING_CONNECT]->setEnabled(false);
				actions[TRACING_REFINE]->setEnabled(false);
				actions[TRACING_DONE]->setEnabled(false);
				actions[TRACING_BRANCH]->setEnabled(false);
				actions[TRACING_EXTEND]->setEnabled(false);
				actions[TRACING_MARK1]->setEnabled(false);
				actions[TRACING_MARK2]->setEnabled(false);
				actions[TRACING_MARK3]->setEnabled(false);
				actions[TRACING_MARK_CLEAR]->setEnabled(false);
				actions[TRACING_DELETE]->setEnabled(false);
				actions[VIEW_SLICE]->setEnabled(false);
				actions[VIEW_EDGE]->setEnabled(false);
				actions[DEBUG_SAVE_CUBE]->setEnabled(false);
				actions[DEBUG_SCREENSHOT]->setEnabled(false);
				actions[DEBUG_START_ROTATE]->setEnabled(false);
				break;
			case SessionState::LoadingCatalog:
				actions[SESSION_SAVE]->setEnabled(ver!=verFile);
				actions[SESSION_SAVE_AS]->setEnabled(true);
				menuMisc->setEnabled(false);
				actions[SESSION_CLOSE]->setEnabled(true);
				actions[EDIT_UNDO]->setEnabled(false);
				actions[EDIT_REDO]->setEnabled(false);
				actions[EDIT_GOTO_POS]->setEnabled(false);
				actions[EDIT_GOTO_TARGET]->setEnabled(false);
				actions[EDIT_PICK_CURRENT]->setEnabled(false);
				actions[EDIT_LOAD_DATA]->setEnabled(false);
				actions[EDIT_LOAD_DATA_AUTO]->setEnabled(false);
				actions[EDIT_CHANGE_TYPE]->setEnabled(false);
				actions[EDIT_CHANGE_MARK]->setEnabled(false);
				actions[EDIT_CHANGE_SIZE]->setEnabled(false);
				actions[EDIT_NEW_NEURON]->setEnabled(false);
				actions[EDIT_RENAME_NEURON]->setEnabled(false);
				actions[EDIT_DEL_NEURONS]->setEnabled(false);
				actions[EDIT_PURGE_NEURONS]->setEnabled(false);
				actions[EDIT_IMPORT_FNT]->setEnabled(false);
				actions[EDIT_IMPORT_SWC]->setEnabled(false);
				actions[EDIT_ABORT]->setEnabled(true);
				actions[TRACING_FIND]->setEnabled(false);
				actions[TRACING_CONNECT]->setEnabled(false);
				actions[TRACING_REFINE]->setEnabled(false);
				actions[TRACING_DONE]->setEnabled(false);
				actions[TRACING_BRANCH]->setEnabled(false);
				actions[TRACING_EXTEND]->setEnabled(false);
				actions[TRACING_MARK1]->setEnabled(false);
				actions[TRACING_MARK2]->setEnabled(false);
				actions[TRACING_MARK3]->setEnabled(false);
				actions[TRACING_MARK_CLEAR]->setEnabled(false);
				actions[TRACING_DELETE]->setEnabled(false);
				actions[VIEW_SLICE]->setEnabled(false);
				actions[VIEW_EDGE]->setEnabled(false);
				actions[DEBUG_SAVE_CUBE]->setEnabled(false);
				actions[DEBUG_SCREENSHOT]->setEnabled(false);
				actions[DEBUG_START_ROTATE]->setEnabled(false);
				break;
			case SessionState::Readonly:
				{
					actions[SESSION_SAVE]->setEnabled(ver!=verFile);
					actions[SESSION_SAVE_AS]->setEnabled(true);
					menuMisc->setEnabled(false);
					actions[SESSION_CLOSE]->setEnabled(true);
					actions[EDIT_UNDO]->setEnabled(undoList.size()>0);
					actions[EDIT_REDO]->setEnabled(redoList.size()>0);
					actions[EDIT_GOTO_POS]->setEnabled(false);
					actions[EDIT_GOTO_TARGET]->setEnabled(false);
					actions[EDIT_PICK_CURRENT]->setEnabled(false);
					actions[EDIT_LOAD_DATA]->setEnabled(false);
					actions[EDIT_LOAD_DATA_AUTO]->setEnabled(false);
					actions[EDIT_CHANGE_TYPE]->setEnabled(false);
					actions[EDIT_CHANGE_MARK]->setEnabled(false);
					actions[EDIT_CHANGE_SIZE]->setEnabled(false);
					actions[EDIT_NEW_NEURON]->setEnabled(false);
					actions[EDIT_RENAME_NEURON]->setEnabled(false);
					actions[EDIT_DEL_NEURONS]->setEnabled(false);
					actions[EDIT_PURGE_NEURONS]->setEnabled(false);
					actions[EDIT_IMPORT_FNT]->setEnabled(false);
					actions[EDIT_IMPORT_SWC]->setEnabled(false);
					actions[EDIT_ABORT]->setEnabled(false);
					actions[TRACING_FIND]->setEnabled(false);
					actions[TRACING_CONNECT]->setEnabled(false);
					actions[TRACING_REFINE]->setEnabled(false);
					actions[TRACING_DONE]->setEnabled(false);
					actions[TRACING_BRANCH]->setEnabled(false);
					actions[TRACING_EXTEND]->setEnabled(false);
					actions[TRACING_MARK1]->setEnabled(false);
					actions[TRACING_MARK2]->setEnabled(false);
					actions[TRACING_MARK3]->setEnabled(false);
					actions[TRACING_MARK_CLEAR]->setEnabled(false);
					actions[TRACING_DELETE]->setEnabled(false);
					actions[VIEW_SLICE]->setEnabled(true);
					actions[VIEW_EDGE]->setEnabled(true);
					actions[DEBUG_SAVE_CUBE]->setEnabled(true);
					actions[DEBUG_SCREENSHOT]->setEnabled(true);
					actions[DEBUG_START_ROTATE]->setEnabled(true);
				}
				break;
			case SessionState::Ready:
				{
					int16_t tgtMark=-1;
					bool tgtEdge=false;
					if(tgtPos.point.valid() && tgtPos.edge) {
						tgtEdge=true;
						tgtMark=tgtPos.edge.points()[tgtPos.index].m;
					}
					size_t nrnidx=0;
					for(auto n: graph.trees()) {
						if(n.selected())
							nrnidx++;
					}
					bool curValid=curPos.point.valid();
					bool tgtValid=tgtPos.point.valid();
					actions[SESSION_SAVE]->setEnabled(ver!=verFile);
					actions[SESSION_SAVE_AS]->setEnabled(true);
					menuMisc->setEnabled(true);
					actions[SESSION_CLOSE]->setEnabled(true);
					actions[EDIT_UNDO]->setEnabled(undoList.size()>0);
					actions[EDIT_REDO]->setEnabled(redoList.size()>0);
					actions[EDIT_GOTO_POS]->setEnabled(true);
					actions[EDIT_GOTO_TARGET]->setEnabled(tgtPos.point.valid());
					actions[EDIT_PICK_CURRENT]->setEnabled(true);
					actions[EDIT_LOAD_DATA]->setEnabled(true);
					actions[EDIT_LOAD_DATA]->setEnabled(true);
					actions[EDIT_LOAD_DATA_AUTO]->setEnabled(true);
					actions[EDIT_CHANGE_TYPE]->setEnabled(tgtEdge);
					actions[EDIT_CHANGE_MARK]->setEnabled(tgtEdge);
					actions[EDIT_CHANGE_SIZE]->setEnabled(tgtEdge);
					actions[EDIT_NEW_NEURON]->setEnabled(tgtEdge && (tgtPos.index==0 || tgtPos.index==tgtPos.edge.length()-1));
					actions[EDIT_RENAME_NEURON]->setEnabled(curRow>=0);
					actions[EDIT_DEL_NEURONS]->setEnabled(nrnidx>0);
					actions[EDIT_PURGE_NEURONS]->setEnabled(nrnidx>0);
					actions[EDIT_IMPORT_FNT]->setEnabled(true);
					actions[EDIT_IMPORT_SWC]->setEnabled(true);
					actions[EDIT_ABORT]->setEnabled(false);
					actions[TRACING_FIND]->setEnabled(curValid);
					actions[TRACING_CONNECT]->setEnabled(curValid&&tgtValid);
					actions[TRACING_REFINE]->setEnabled(path.points.size()>=2);
					actions[TRACING_DONE]->setEnabled(true);
					actions[TRACING_BRANCH]->setEnabled(path.points.size()>=2);
					actions[TRACING_EXTEND]->setEnabled(path.points.size()>=2);
					actions[TRACING_MARK1]->setEnabled(tgtEdge && tgtMark!=1);
					actions[TRACING_MARK2]->setEnabled(tgtEdge && tgtMark!=2);
					actions[TRACING_MARK3]->setEnabled(tgtEdge && tgtMark!=3);
					actions[TRACING_MARK_CLEAR]->setEnabled(tgtEdge && tgtMark!=0);
					actions[TRACING_DELETE]->setEnabled(curPos.edge || tgtEdge);
					actions[VIEW_SLICE]->setEnabled(true);
					actions[VIEW_EDGE]->setEnabled(true);
					actions[DEBUG_SAVE_CUBE]->setEnabled(true);
					actions[DEBUG_SCREENSHOT]->setEnabled(true);
					actions[DEBUG_START_ROTATE]->setEnabled(true);
				}
				break;
			case SessionState::Computing:
				{
					actions[SESSION_SAVE]->setEnabled(ver!=verFile);
					actions[SESSION_SAVE_AS]->setEnabled(true);
					menuMisc->setEnabled(true);
					actions[SESSION_CLOSE]->setEnabled(true);
					actions[EDIT_UNDO]->setEnabled(false);
					actions[EDIT_REDO]->setEnabled(false);
					actions[EDIT_GOTO_POS]->setEnabled(true);
					actions[EDIT_GOTO_TARGET]->setEnabled(tgtPos.point.valid());
					actions[EDIT_PICK_CURRENT]->setEnabled(true);
					actions[EDIT_LOAD_DATA]->setEnabled(false);
					actions[EDIT_LOAD_DATA_AUTO]->setEnabled(true);
					actions[EDIT_CHANGE_TYPE]->setEnabled(false);
					actions[EDIT_CHANGE_MARK]->setEnabled(false);
					actions[EDIT_CHANGE_SIZE]->setEnabled(false);
					actions[EDIT_NEW_NEURON]->setEnabled(false);
					actions[EDIT_RENAME_NEURON]->setEnabled(false);
					actions[EDIT_DEL_NEURONS]->setEnabled(false);
					actions[EDIT_PURGE_NEURONS]->setEnabled(false);
					actions[EDIT_IMPORT_FNT]->setEnabled(false);
					actions[EDIT_IMPORT_SWC]->setEnabled(false);
					actions[EDIT_ABORT]->setEnabled(true);
					actions[TRACING_FIND]->setEnabled(false);
					actions[TRACING_CONNECT]->setEnabled(false);
					actions[TRACING_REFINE]->setEnabled(false);
					actions[TRACING_DONE]->setEnabled(false);
					actions[TRACING_BRANCH]->setEnabled(false);
					actions[TRACING_EXTEND]->setEnabled(false);
					actions[TRACING_MARK1]->setEnabled(false);
					actions[TRACING_MARK2]->setEnabled(false);
					actions[TRACING_MARK3]->setEnabled(false);
					actions[TRACING_MARK_CLEAR]->setEnabled(false);
					actions[TRACING_DELETE]->setEnabled(false);
					actions[VIEW_SLICE]->setEnabled(true);
					actions[VIEW_EDGE]->setEnabled(true);
					actions[DEBUG_SAVE_CUBE]->setEnabled(true);
					actions[DEBUG_SCREENSHOT]->setEnabled(true);
					actions[DEBUG_START_ROTATE]->setEnabled(true);
				}
				break;
			case SessionState::LoadingCubes:
				{
					int16_t tgtMark=-1;
					bool tgtEdge=false;
					if(tgtPos.point.valid() && tgtPos.edge) {
						tgtEdge=true;
						tgtMark=tgtPos.edge.points()[tgtPos.index].m;
					}
					size_t nrnidx=0;
					for(auto n: graph.trees()) {
						if(n.selected())
							nrnidx++;
					}
					actions[SESSION_SAVE]->setEnabled(ver!=verFile);
					actions[SESSION_SAVE_AS]->setEnabled(true);
					menuMisc->setEnabled(true);
					actions[SESSION_CLOSE]->setEnabled(true);
					actions[EDIT_UNDO]->setEnabled(undoList.size()>0);
					actions[EDIT_REDO]->setEnabled(redoList.size()>0);
					actions[EDIT_GOTO_POS]->setEnabled(true);
					actions[EDIT_GOTO_TARGET]->setEnabled(tgtPos.point.valid());
					actions[EDIT_PICK_CURRENT]->setEnabled(true);
					actions[EDIT_LOAD_DATA]->setEnabled(false);
					actions[EDIT_LOAD_DATA_AUTO]->setEnabled(true);
					actions[EDIT_CHANGE_TYPE]->setEnabled(tgtEdge);
					actions[EDIT_CHANGE_MARK]->setEnabled(tgtEdge);
					actions[EDIT_CHANGE_SIZE]->setEnabled(tgtEdge);
					actions[EDIT_NEW_NEURON]->setEnabled(tgtEdge && (tgtPos.index==0 || tgtPos.index==tgtPos.edge.length()-1));
					actions[EDIT_RENAME_NEURON]->setEnabled(curRow>=0);
					actions[EDIT_DEL_NEURONS]->setEnabled(nrnidx>0);
					actions[EDIT_PURGE_NEURONS]->setEnabled(nrnidx>0);
					actions[EDIT_IMPORT_FNT]->setEnabled(true);
					actions[EDIT_IMPORT_SWC]->setEnabled(true);
					actions[EDIT_ABORT]->setEnabled(true);
					actions[TRACING_FIND]->setEnabled(false);
					actions[TRACING_CONNECT]->setEnabled(false);
					actions[TRACING_REFINE]->setEnabled(false);
					actions[TRACING_DONE]->setEnabled(true);
					actions[TRACING_BRANCH]->setEnabled(path.points.size()>=2);
					actions[TRACING_EXTEND]->setEnabled(path.points.size()>=2);
					actions[TRACING_MARK1]->setEnabled(tgtEdge && tgtMark!=1);
					actions[TRACING_MARK2]->setEnabled(tgtEdge && tgtMark!=2);
					actions[TRACING_MARK3]->setEnabled(tgtEdge && tgtMark!=3);
					actions[TRACING_MARK_CLEAR]->setEnabled(tgtEdge && tgtMark!=0);
					actions[TRACING_DELETE]->setEnabled(curPos.edge || tgtEdge);
					actions[VIEW_SLICE]->setEnabled(true);
					actions[VIEW_EDGE]->setEnabled(true);
					actions[DEBUG_SAVE_CUBE]->setEnabled(true);
					actions[DEBUG_SCREENSHOT]->setEnabled(true);
					actions[DEBUG_START_ROTATE]->setEnabled(true);
				}
				break;
		}
	}
	void setState(SessionState stt, const QString& desc, bool p) {
		state=stt;
		if(p) {
			progr->setMaximum(0);
			progr->setValue(0);
			progr->setEnabled(true);
		} else {
			progr->setEnabled(false);
			progr->setMaximum(1);
			progr->setValue(0);
		}
		statLabel->setText(desc);
		//setCursor(QCursor(Qt::ArrowCursor));
		updateActions();
		Q_EMIT session->stateChanged(session, state);
	}
	void setFile(const QString& s) {
		file=s;
		if(file.isNull()) {
			if(ver!=verFile) {
				session->setWindowTitle("[Untitled]*");
			} else {
				session->setWindowTitle("[Untitled]");
			}
		} else {
			if(ver!=verFile) {
				session->setWindowTitle(QDir{file}.dirName()+'*');
			} else {
				session->setWindowTitle(QDir{file}.dirName());
			}
		}
	}
	void updateAnnotLabel() {
		double pos[3]={tgtPos.point.x(), tgtPos.point.y(), tgtPos.point.z()};
		for(int i=0; i<3; i++) {
			pos[i]-=annotCube.xform.origin[i]+annotCube.xform.direction[i]*annotCube.data.cubeId().x0+annotCube.xform.direction[i+3]*annotCube.data.cubeId().y0+annotCube.xform.direction[i+6]*annotCube.data.cubeId().z0;
		}
		int32_t posi[3];
		for(int i=0; i<3; i++) {
			posi[i]=pos[0]*annotCube.xform.direction_inv[i]+pos[1]*annotCube.xform.direction_inv[3+i]+pos[2]*annotCube.xform.direction_inv[6+i];
		}
		if(posi[0]<0||posi[0]>=annotCube.data.width())
			return;
		if(posi[1]<0||posi[0]>=annotCube.data.height())
			return;
		if(posi[2]<0||posi[0]>=annotCube.data.depth())
			return;
		auto annoti=posi[0]+annotCube.data.widthAdj()*(posi[1]+annotCube.data.heightAdj()*posi[2]);
		uint32_t id;
		switch(annotCube.data.type()) {
			case CubeType::U8:
				id=reinterpret_cast<const uint8_t*>(annotCube.data.data())[annoti];
				break;
			case CubeType::U16:
				id=reinterpret_cast<const uint16_t*>(annotCube.data.data())[annoti];
				break;
			case CubeType::U32:
				id=reinterpret_cast<const uint32_t*>(annotCube.data.data())[annoti];
				break;
			default:
				id=0;
		}
		std::vector<const AnnotItem*> annots;
		auto n=annotTree->annot(id);
		while(n) {
			annots.push_back(n);
			n=annotTree->annot(n->par_idx);
		}
		annotHeader->clear();
		while(!annots.empty()) {
			auto name=QString::fromStdString(annots.back()->name);
			annotHeader->addItem(name);
			annots.pop_back();
		}
		annotHeader->setCurrentRow(annotHeader->count()-1);
	}
	void pickPosition(Edge edg, size_t idx, const Point* pos) {
		bool chg=false;
		if(tgtPos.edge!=edg) {
			tgtPos.edge=edg;
			chg=true;
		}
		const Point* p=nullptr;
		if(edg) {
			if(tgtPos.index!=idx) {
				tgtPos.index=idx;
				chg=true;
			}
			p=&edg.points()[idx];
		} else {
			p=pos;
		}
		if(p) {
			if(!(tgtPos.point==*p)) {
				tgtPos.point=*p;
				chg=true;
			}
		}
		if(chg) {
			updateSlider(false, tgtPos);
			viewer->updatePosition(tgtPos, false);
			if(annotCube.data)
				updateAnnotLabel();
			if(tgtPos.point.valid() && tgtPos.edge && tgtPos.edge.tree()) {
				curRow=tgtPos.edge.tree().index();
				auto idx=listModel->index(curRow, 0, {});
				listView->selectionModel()->setCurrentIndex(idx, QItemSelectionModel::Clear|QItemSelectionModel::SelectCurrent|QItemSelectionModel::Rows);
			}
		}
	} // update(actions/viewer)
	// XXX do this when graph changed(edge changed): priv->path.path.clear();
	// change current edge binding
	// change target edge binding
	void jumpToPosition(Edge edg, size_t idx, const Point* pos) {
		curPos.edge=edg;
		const Point* p=nullptr;
		if(edg) {
			curPos.index=idx;
			p=&edg.points()[idx];
		} else {
			p=pos;
		}
		//
		if(p) {
			if(p->valid()) {
				curPos.point=*p;
				viewer->clearSelection();
			}
		}
		updateSlider(true, curPos);
		path.points.clear(); // path is valid only when it's in current cube
		if(viewer) {
			viewer->updatePosition(curPos, true);
			viewer->setPathData(path);
		}
		if(computeThread) {
			computeThread->positionChanged();
		}
	}
	void updateVersion(int64_t v, bool branching) {
		if(branching) {
			if(verFile>ver)
				verFile=-2;
			if(verSwap>ver)
				verSwap=-2;
		}
		ver+=v;
		setFile(file);
	}
	void runCommand(Command* cmd) {
		if(redoList.size()>0) {
			for(auto p: redoList) {
				delete p;
			}
			redoList.clear();
		}
		cmd->redo(this);
		undoList.push_front(cmd);
		while(undoList.size()>100) {
			auto p=undoList.back();
			delete p;
			undoList.pop_back();
		}
		updateVersion(1, true);
		updateActions();
		viewer->update();
		if(state==SessionState::Ready && loadDataAuto)
			session->startDownloadCube();
	}

	bool attachOptionsDlg() {
		viewer->attachOptionsDlg();
		computeThread->attachOptionsDlg();
		SessionChannelOptions::instance()->attach(this);
		SessionLoadingOptions::instance()->attach(this);
		return true;
	}
	bool detachOptionsDlg() {
		viewer->detachOptionsDlg();
		computeThread->detachOptionsDlg();
		SessionChannelOptions::instance()->attach(nullptr);
		SessionLoadingOptions::instance()->attach(nullptr);
		return true;
	}

	bool updateCube(int ch, int idx, const std::pair<double, double>& xfunc) {
		if(idx<=0) {
			cube_maps[ch]=idx;
			cubes[ch]=Cube{};
			viewer->updateCube(ch, cubes[ch], xfunc);
			if(ch%3==0)
				computeThread->updateCube(ch/3, cubes[ch], xfunc);
		} else if(idx!=cube_maps[ch]) {
			cube_maps[ch]=idx;
			cubes[ch]=Cube{};
			xfuncs[ch]=xfunc;
			viewer->updateCube(ch, cubes[ch], xfuncs[ch]);
			if(ch%3==0)
				computeThread->updateCube(ch/3, cubes[ch], xfuncs[ch]);
		} else {
			xfuncs[ch]=xfunc;
			viewer->updateXfunc(ch, xfuncs[ch]);
			if(ch%3==0)
				computeThread->updateXfunc(ch/3, xfuncs[ch]);
		}
		return true;
	}
	bool updateXfunc(int ch, const std::pair<double, double>& xfunc) {
		if(cube_maps[ch]<=0) {
			viewer->updateXfunc(ch, xfunc);
			if(ch%3==0)
				computeThread->updateXfunc(ch/3, xfunc);
		} else {
			xfuncs[ch]=xfunc;
			viewer->updateXfunc(ch, xfuncs[ch]);
			if(ch%3==0)
				computeThread->updateXfunc(ch/3, xfuncs[ch]);
		}
		return true;
	}
	bool startDownloadCube() {
		switch(state) {
			case SessionState::Invalid:
			case SessionState::LoadingCatalog:
			case SessionState::Readonly:
			case SessionState::Computing:
			case SessionState::LoadingCubes:
				return false;
			case SessionState::Ready:
				break;
		}
		bool needDownload=false;
		for(int i=0; i<9; i++) {
			if(cube_maps[i]!=0) {
				if(loadThread->setPosition(i, cube_maps[i], curPos.point.x(), curPos.point.y(), curPos.point.z(), sight, cubes[i])) {
					printMessage("Need to download ", cube_maps[i], '/', i);
					needDownload=true;
				}
			}
		}
		if(annot_map!=0) {
			if(loadThread->setPosition(10, annot_map, curPos.point.x(), curPos.point.y(), curPos.point.z(), sight, annotCube)) {
				printMessage("Need to download ", annot_map);
				needDownload=true;
			}
		}
		if(needDownload) {
			setState(SessionState::LoadingCubes, "Downloading cubes.", true);
		}
		return true;
	}

	void viewerReady(bool err) {
		if(err) {
			showWarning("Failed to initialize 3D viewer", "Failed to load 3D viewer.", session);
		} else {
			postConstruct();
		}
		Q_EMIT session->sessionReady(session, err);
	}

	void loadOptions() {
		auto goptions=Tracer::instance()->options();

		int loaddataauto=1;
		if(!options.getInt("core.auto.load.data", &loaddataauto))
			goptions->getInt("core.auto.load.data", &loaddataauto);
		loadDataAuto=loaddataauto;

		int idx, edg;
		if(options.getInt("core.target.edge", &edg)
				&& options.getInt("core.target.idx", &idx)) {
			size_t ei=edg, pi=idx;
			if(ei<graph.edges().size() && pi<graph.edges()[ei].length()) {
				tgtPos.edge=graph.edges()[ei];
				tgtPos.index=pi;
				tgtPos.point=tgtPos.edge.points()[pi];
			} else {
				tgtPos.point.m=-1;
			}
		} else if(options.getXyzInt("core.target.pos", &tgtPos.point._x, &tgtPos.point._y, &tgtPos.point._z)) {
			tgtPos.point.m=0;
		} else {
			tgtPos.point.m=-1;
		}
		if(options.getInt("core.current.edge", &edg)
				&& options.getInt("core.current.idx", &idx)) {
			size_t ei=edg, pi=idx;
			if(ei<graph.edges().size() && pi<graph.edges()[ei].length()) {
				curPos.edge=graph.edges()[ei];
				curPos.index=pi;
				curPos.point=curPos.edge.points()[pi];
			} else {
				curPos.point.m=-1;
			}
		} else if(options.getXyzInt("core.current.pos", &curPos.point._x, &curPos.point._y, &curPos.point._z)) {
			curPos.point.m=0;
		} else {
			curPos.point.m=-1;
		}

		//
		// XXX Options::config
		//options.getDouble("core.cube.size", &volume.sight);
		//volume.setSight(volume.sight);
	}
	void saveOptions() {
		{
			if(annot_map>0) {
				options.setInt("channel.map.annot", annot_map);
			} else {
				options.removeKey("channel.map.annot");
			}
		}
		SessionChannelOptions::instance()->saveConfig(this, &options);
		SessionLoadingOptions::instance()->saveConfig(this, &options);
		viewer->saveOptions(&options);
		computeThread->saveOptions(&options);
		// XXX OptionsSight::saveConfig
		//XXX pull options

		if(tgtPos.point.valid()) {
			if(tgtPos.edge) {
				options.setInt("core.target.edge", tgtPos.edge.index());
				options.setInt("core.target.idx", tgtPos.index);
			} else {
				options.setXyzInt("core.target.pos", tgtPos.point._x, tgtPos.point._y, tgtPos.point._z);
				options.removeKey("core.target.edge");
			}
		} else {
			options.removeKey("core.target.edge");
			options.removeKey("core.target.pos.x");
		}
		if(curPos.point.valid()) {
			if(curPos.edge) {
				options.setInt("core.current.edge", curPos.edge.index());
				options.setInt("core.current.idx", curPos.index);
			} else {
				options.setXyzInt("core.current.pos", curPos.point._x, curPos.point._y, curPos.point._z);
				options.removeKey("core.current.edge");
			}
		} else {
			options.removeKey("core.current.edge");
			options.removeKey("core.current.pos.x");
		}

		options.setInt("core.auto.load.data", loadDataAuto);
	}

	bool updateBoundingBox(double x, double y, double z) {
		bool expanded{false};
		if(x<bbox[0]) {
			bbox[0]=x; expanded=true;
		}
		if(x>bbox[1]) {
			bbox[1]=x; expanded=true;
		}
		if(y<bbox[2]) {
			bbox[2]=y; expanded=true;
		}
		if(y>bbox[3]) {
			bbox[3]=y; expanded=true;
		}
		if(z<bbox[4]) {
			bbox[4]=z; expanded=true;
		}
		if(z>bbox[5]) {
			bbox[5]=z; expanded=true;
		}
		return expanded;
	}
	bool updateBoundingBox(const Cube& cube) {
		int32_t p0[]={cube.data.cubeId().x0, cube.data.cubeId().y0, cube.data.cubeId().z0};
		double pl[3];
		double pr[3];
		for(int dir=0; dir<3; dir++) {
			double rr=cube.xform.origin[dir];
			double ll=rr;
			for(int k=0; k<3; k++) {
				ll+=cube.xform.direction[dir+k*3]*p0[k];
				rr+=cube.xform.direction[dir+k*3]*p0[k];
				if(cube.xform.direction[dir+k*3]>=0) {
					rr+=cube.xform.direction[dir+k*3]*cube.data.size(k);
				} else {
					ll+=cube.xform.direction[dir+k*3]*cube.data.size(k);
				}
			}
			pl[dir]=ll;
			pr[dir]=rr;
		}
		return updateBoundingBox(pl[0], pl[1], pl[2]) || updateBoundingBox(pr[0], pr[1], pr[2]);
	}
	void updateAnnotModel(const AnnotTree* t) {
		auto prevm=annotModel;
		if(prevm && annotTree==t)
			return;

		annotTree=t;
		annotModel=new AnnotModel{session, *t};
		annotView->setModel(annotModel);
		QObject::connect(annotView->selectionModel(), &QItemSelectionModel::currentRowChanged, session, &Session::currentChanged);
		annotView->setEnabled(true);
		//QObject::connect(annotView->selectionModel(), &QItemSelectionModel::selectionChanged, session, &Session::selectionChanged);
		if(prevm)
			delete prevm;
	}
	void annotListChanged(int idx) {
		if(idx!=annot_map) {
			annot_map=idx;
			annotView->setModel(nullptr);
			if(annotModel) {
				delete annotModel;
				annotModel=nullptr;
				annotTree=nullptr;
			}
			annotCube=Cube{};
			if(loadDataAuto)
				startDownloadCube();
		}
	}
};
void Session::attachOptionsDlg() { priv->attachOptionsDlg(); }
void Session::detachOptionsDlg() { priv->detachOptionsDlg(); }
SessionState Session::state() const { return priv->state; }
void Session::viewerReady(bool err) { priv->viewerReady(err); }

void Session::renameNeuron() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::LoadingCubes:
		case SessionState::Ready:
			break;
	}
	if(priv->curRow<0 || static_cast<size_t>(priv->curRow)>=priv->graph.trees().size())
		return;
	Tree n=priv->graph.trees()[priv->curRow];
	bool ok;
	auto prevname=QString::fromStdString(n.name());
	auto name=QInputDialog::getText(this, "Rename Neuron", "Please give it a new name.", QLineEdit::Normal, prevname, &ok);
	if(!ok)
		return;
	if(prevname==name)
		return;
	auto cmd=Command::renameNeuron(priv, n, name.toStdString());
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "EDIT_NEURON_RENAME");
}

void Session::jumpPosition() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
			return;
		case SessionState::Computing:
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	Point pos=priv->curPos.point;
	if(!PositionDialog::getPosition(&pos, priv->bbox, priv->session))
		return;
	priv->jumpToPosition(Edge{}, -1, &pos);
	priv->updateActions();
	priv->viewer->update();
	if(priv->state==SessionState::Ready && priv->loadDataAuto)
		startDownloadCube();
	Tracer::instance()->logMessage("STAT: ", "EDIT_GOTO_POS");
}
void Session::jumpToTarget() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
			return;
		case SessionState::Ready:
		case SessionState::Computing:
		case SessionState::LoadingCubes:
			break;
	}
	if(!priv->tgtPos.point.valid())
		return;
	priv->jumpToPosition(priv->tgtPos.edge, priv->tgtPos.index, &priv->tgtPos.point);
	priv->updateActions();
	priv->viewer->update();
	if(priv->state==SessionState::Ready && priv->loadDataAuto)
		startDownloadCube();
	Tracer::instance()->logMessage("STAT: ", "EDIT_GOTO_TARGET");
}
void Session::pickCurrent() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
			return;
		case SessionState::Ready:
		case SessionState::Computing:
		case SessionState::LoadingCubes:
			break;
	}
	if(!priv->curPos.point.valid())
		return;
	priv->pickPosition(priv->curPos.edge, priv->curPos.index, &priv->curPos.point);
	priv->viewer->update();
	priv->updateActions();
	Tracer::instance()->logMessage("STAT: ", "EDIT_PICK_CURRENT");
}
void Session::startDownloadCube() {
	priv->startDownloadCube();
	Tracer::instance()->logMessage("STAT: ", "EDIT_LOAD_DATA");
}

void Session::toggleAutoDownload(bool r) {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
			return;
		case SessionState::Computing:
		case SessionState::LoadingCubes:
		case SessionState::Ready:
			break;
	}
	priv->loadDataAuto=r;
	if(r)
		startDownloadCube();
}
Command::Command(SessionPriv* tp, const QString& t):
	_sp{tp}, _title{t}, _performed{false}, _curChg{false}, _tgtChg{false}
{
	_curPos=tp->curPos;
	_tgtPos=tp->tgtPos;
}
void Command::updateBuffer(EdgePriv* ep) {
	_sp->viewer->updateEdge(ep);
}
void Command::updateBuffer(EdgePriv* ep, size_t i) {
	_sp->viewer->updateEdgeNode(ep, i);
}
void Command::freeBuffer(EdgePriv* ep) {
	_sp->viewer->recycleEdge(ep);
}
Edge mapEdgeHelper(Edge esrc, size_t isrc, Edge edst, size_t dst0, bool dir, size_t* idst) {
	if(dir) {
		if(dst0-isrc<edst.points().size()) {
			*idst=dst0-isrc;
			return edst;
		}
	} else {
		if(isrc-dst0<edst.points().size()) {
			*idst=isrc-dst0;
			return edst;
		}
	}
	return Edge{};
}
void Command::mapEdgeMultiple(Edge e, std::vector<std::tuple<Edge, size_t, bool>> dats) {
	if(curPos().edge==e) {
		bool hit=false;
		for(auto& t: dats) {
			size_t eei;
			auto ee=mapEdgeHelper(e, curPos().index, std::get<0>(t), std::get<1>(t), std::get<2>(t), &eei);
			if(ee) {
				Position p{ee, eei, Point{}};
				setCurPos(p);
				hit=true;
				break;
			}
		}
		if(!hit) {
			Position p{Edge{}, 0, curPos().point};
			setCurPos(p);
		}
	}
	if(tgtPos().edge==e) {
		bool hit=false;
		for(auto& t: dats) {
			size_t eei;
			auto ee=mapEdgeHelper(e, tgtPos().index, std::get<0>(t), std::get<1>(t), std::get<2>(t), &eei);
			if(ee) {
				Position p{ee, eei, Point{}};
				setTgtPos(p);
				hit=true;
				break;
			}
		}
		if(!hit) {
			Position p{Edge{}, 0, tgtPos().point};
			setTgtPos(p);
		}
	}
}
void Command::mapEdge(Edge e, Edge ne, size_t i, bool dir) {
	mapEdgeMultiple(e, {std::make_tuple(ne, i, dir)});
}
void dumpGraph(Graph gr) {
	printMessage("\nNV %1 NE %2 NN %3\n", gr.vertices().size(), gr.edges().size(), gr.trees().size());
}
void Command::undo(SessionPriv* tp) {
	undoImp(tp);
	_performed=false;
	tp->path.points.clear();
	tp->viewer->setPathData(tp->path);
	tp->pickPosition(_tgtPos.edge, _tgtPos.index, &_tgtPos.point);
	tp->jumpToPosition(_curPos.edge, _curPos.index, &_curPos.point);
	dumpGraph(tp->graph);
};
void Command::redo(SessionPriv* tp) {
	redoImp(tp);
	_performed=true;
	tp->path.points.clear();
	tp->viewer->setPathData(tp->path);
	if(_tgtChg)
		tp->pickPosition(_tgtPosNew.edge, _tgtPosNew.index, &_tgtPosNew.point);
	if(_curChg)
		tp->jumpToPosition(_curPosNew.edge, _curPosNew.index, &_curPosNew.point);
	dumpGraph(tp->graph);
}
Graph Command::graph() const {
	return _sp->graph;
}
ListModel* Command::model() const {
	return _sp->listModel;
}
bool Command::isTargetPosOnPath() const {
	return _sp->pathConnected || _sp->curPos.point==_sp->tgtPos.point;
}
void Session::changeType() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	if(!priv->tgtPos.edge)
		return;
	int curr=priv->tgtPos.edge.type();
	if(curr) curr-=1; // (0, 2, 3, 4) -> (0, 1, 2, 3)
	bool ok;
	QStringList items;
	items<<"Undefined"<<"Axon"<<"Dendrite"<<"Apical dendrite";
	auto res=QInputDialog::getItem(this, "Choose neurite type", "Please choose a neurite type.", items, curr, false, &ok);
	if(!ok)
		return;
	curr=items.indexOf(res);
	if(curr==-1) {
		showWarning("Change edge type", "Wrong type: "+res, this);
		return;
	}
	if(curr) curr+=1; // (0, 1, 2, 3) -> (0, 2, 3, 4)
	if(curr==priv->tgtPos.edge.type())
		return;
	auto cmd=Command::changeType(priv, priv->tgtPos.edge, curr);
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "EDIT_CHANGE_TYPE");
}
void Session::changeMark() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	if(!priv->tgtPos.edge)
		return;
	int curr=priv->tgtPos.edge.points()[priv->tgtPos.index].m;
	bool ok;
	auto res=QInputDialog::getInt(this, "Set node mark", "Please choose an integer to mark the node.", curr, 0, 32767, 1, &ok);
	if(!ok)
		return;
	if(res<0) res=0;
	if(res>=32768) res=32767;
	if(res==curr)
		return;
	auto cmd=Command::changeMark(priv, priv->tgtPos.edge, priv->tgtPos.index, static_cast<int16_t>(res));
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "EDIT_CHANGE_MARK");
}
void Session::changeSize() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	if(!priv->tgtPos.edge)
		return;
	auto curr=priv->tgtPos.edge.points()[priv->tgtPos.index]._r;
	bool ok;
	auto res=QInputDialog::getDouble(this, "Set node size", "Please pick a node size (in um).", curr/256.0, 0, 255, 2, &ok);
	if(!ok)
		return;
	int v=lrint(res*256);
	if(v<0) v=0;
	if(v>=65536) v=65535;
	if(v==curr)
		return;
	auto cmd=Command::changeSize(priv, priv->tgtPos.edge, priv->tgtPos.index, static_cast<uint16_t>(v));
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "EDIT_CHANGE_SIZE");
}

void Session::createNeuron() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	if(!priv->tgtPos.edge)
		return;
	Vertex v;
	if(priv->tgtPos.index==0) {
		v=priv->tgtPos.edge.leftVertex();
	} else if(priv->tgtPos.index==priv->tgtPos.edge.points().size()-1) {
		v=priv->tgtPos.edge.rightVertex();
	}
	if(!v)
		return;
	if(v.tree()) {
		showWarning("Create neuron", "This vertex is already part of a neuron.", this);
		return;
	}
	auto cmd=Command::createNeuron(priv, v);
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "EDIT_NEURON_NEW");
}
void Session::removeNeurons() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	std::vector<size_t> idxes{};
	for(auto n: priv->graph.trees()) {
		if(n.selected())
			idxes.emplace_back(n.index());
	}
	if(idxes.size()<=0)
		return;
	auto cmd=Command::removeNeurons(priv, idxes);
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "EDIT_NEURON_REMOVE");
}
void Session::purgeNeurons() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	std::vector<size_t> idxes{};
	for(auto n: priv->graph.trees()) {
		if(n.selected())
			idxes.emplace_back(n.index());
	}
	if(idxes.size()<=0)
		return;
	auto cmd=Command::purgeNeurons(priv, idxes);
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "EDIT_NEURON_PURGE");
}
void Session::importSwc() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}

	QFileDialog dlg{this, "Please choose SWC files.", "", ""};
	dlg.setAcceptMode(QFileDialog::AcceptOpen);
	dlg.setFileMode(QFileDialog::ExistingFiles);
	dlg.setNameFilter("SWC files (*.swc)");
	if(dlg.exec()!=QDialog::Accepted)
		return;

	std::vector<Point> points;
	std::vector<int64_t> parents;
	try {
		for(auto& filename: dlg.selectedFiles()) {
			std::map<int64_t, int64_t> idmap;
			// XXX use ifstream and move to shared?
			QFile sf{filename};
			if(!sf.open(QIODevice::ReadOnly)) {
				throwError("Failed to open file");
			}

			while(!sf.atEnd()) {
				auto line=sf.readLine();
				if(line.isEmpty()) continue;
				if(line[0]=='#') continue;
				QTextStream fs(&line);
				fs.setRealNumberNotation(QTextStream::SmartNotation);
				fs.setRealNumberPrecision(13);
				int64_t id, par;
				int16_t type;
				double x, y, z, r;
				fs>>id>>type>>x>>y>>z>>r>>par;
				if(fs.status()!=QTextStream::Ok)
					throwError(QString{"Failed to parse line %1"}+=line);

				idmap[id]=points.size();
				points.emplace_back(x, y, z, r, type);
				if(par==-1) {
					parents.push_back(-1);
				} else {
					parents.push_back(idmap[par]);
				}
			}
		}
	} catch(const std::exception& e) {
		showWarning("Failed to import SWC", e.what(), this);
		return;
	}
	if(points.size()<=0)
		return;
	auto cmd=Command::importSwc(priv, points, parents);
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "EDIT_IMPORT_SWC");
}
void Session::importFnt() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}

	QFileDialog dlg(this, "Please choose FNT Session files.", "", "");
	dlg.setAcceptMode(QFileDialog::AcceptOpen);
	dlg.setFileMode(QFileDialog::ExistingFiles);
	dlg.setNameFilter("FNT Session files (*.fnt *.fntz)");
	if(dlg.exec()!=QDialog::Accepted)
		return;

	std::vector<bool> vertInfo{};
	std::vector<std::tuple<size_t, size_t, int16_t, std::vector<Point>>> edgInfo{};
	std::vector<std::tuple<size_t, std::string>> somaInfo{};
	try {
		for(auto& filename: dlg.selectedFiles()) {
			auto vertIdxShift=vertInfo.size();

			InputStream fs;
			if(!fs.open(filename.toLocal8Bit().constData()))
				throwError("Failed to open file");
			fs.precision(13);
			fs.setf(std::ios::scientific);


			std::string line;
			std::getline(fs, line);
			if(!fs)
				throwError("Failed to read magic header");
			bool gzipped=false;
			if(line==FNTZ_MAGIC) {
				gzipped=true;
			} else if(line!=FNT_MAGIC) {
				throwError("Wrong magic: ", line);
			}

			std::getline(fs, line);
			if(!fs)
				throwError("Failed to read URL line");

			std::getline(fs, line);
			if(!fs)
				throwError("Failed to read line");
			while(line!="BEGIN_TRACING_DATA") {
				std::getline(fs, line);
				if(!fs)
					throwError("Failed to read line");
			}

			if(gzipped) {
				if(!fs.pushGzip())
					throwError("Failed to start decompressing");
			}
			size_t nv;
			fs>>nv;
			if(!fs)
				throwError("Failed to read size_t");
			for(size_t i=0; i<nv; i++) {
				int f;
				fs>>f;
				vertInfo.push_back(f);
				if(!fs)
					throwError("Failed to read bool");
			}

			size_t ne;
			fs>>ne;
			if(!fs)
				throwError("Failed to read size_t");
			for(size_t i=0; i<ne; i++) {
				size_t eli, eri, epl;
				int16_t type;
				fs>>type;
				fs>>eli;
				fs>>eri;
				fs>>epl;
				if(!fs)
					throwError("Failed to read ints");
				std::vector<Point> pts;
				for(size_t j=0; j<epl; j++) {
					pts.push_back(Point{});
					auto& p=pts.back();
					fs>>p.m>>p._x>>p._y>>p._z>>p._r;
					if(!fs)
						throwError("Failed to read point");
				}
				edgInfo.emplace_back(eli+vertIdxShift, eri+vertIdxShift, type, std::move(pts));
			}
			size_t nn;
			fs>>nn;
			if(!fs)
				throwError("Failed to read size_t");
			for(size_t i=0; i<nn; i++) {
				size_t si;
				fs>>si;
				std::string name;
				std::getline(std::ws(fs), name);
				if(!fs)
					throwError("Failed to read neuron");
				somaInfo.emplace_back(si+vertIdxShift, name);
			}
			if(gzipped) {
				if(!fs.pop())
					throwError("Failed to finish decompressing");
			}
			if(!fs.close())
				throwError("Failed to close file.");
		}
	} catch(const std::exception& e) {
		showWarning("Failed to import FNT", e.what(), this);
		return;
	}
	if(vertInfo.size()<=0 && edgInfo.size()<=0 && somaInfo.size()<=0)
		return;
	auto cmd=Command::importFnt(priv, vertInfo, edgInfo, somaInfo);
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "EDIT_IMPORT_FNT");
}

void Session::findPath() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
		case SessionState::LoadingCubes:
			return;
		case SessionState::Ready:
			break;
	}
	if(!priv->curPos.point.valid())
		return;
	Tracer::instance()->logMessage("STAT: ", "TRACING_FIND");
	priv->computeThread->find(priv->curPos, priv->graph);
	priv->setState(SessionState::Computing, "Finding a path...", true);
}
void Session::connectPoints() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
		case SessionState::LoadingCubes:
			return;
		case SessionState::Ready:
			break;
	}
	if(!priv->curPos.point.valid())
		return;
	if(!priv->tgtPos.point.valid())
		return;
	Tracer::instance()->logMessage("STAT: ", "TRACING_CONNECT");
	priv->computeThread->connect(priv->curPos, priv->tgtPos, priv->graph);
	priv->setState(SessionState::Computing, "Connect two points...", true);
}
void Session::refinePath() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
		case SessionState::LoadingCubes:
			return;
		case SessionState::Ready:
			break;
	}
	if(priv->path.points.size()<2)
		return;
	Tracer::instance()->logMessage("STAT: ", "TRACING_REFINE");
	priv->computeThread->refine(priv->path, priv->graph);
	priv->setState(SessionState::Computing, "Refining the path...", true);
}

Edge getNextPosition(Graph gr, const Point& curPos, size_t* idx) {
	struct pos_sort {
		Edge e;
		int l;
		size_t i;
		double d;
	};
	std::vector<pos_sort> poss{};
	for(auto v: gr.vertices()) {
		if(v.inLoop()) {
			bool atend=true;
			for(auto& p: v.neighbors()) {
				if(p.first.inLoop() && p.first.parentVertex()==v) {
					atend=false;
					break;
				}
			}
			if(atend) {
				auto e=v.parentEdge();
				pos_sort pos{e, 0, 0};
				if(e.leftVertex()!=v) {
					pos.i=e.points().size()-1;
				}
				pos.d=curPos.distTo(e.points()[pos.i]);
				if(pos.d>0)
					poss.emplace_back(pos);
			}
		} else if(!v.finished()) {
			auto& ep=v.neighbors()[0];
			auto e=ep.first;
			pos_sort pos{e, 1, 0};
			if(ep.second) {
				pos.i=e.points().size()-1;
			}
			pos.d=curPos.distTo(e.points()[pos.i]);
			if(pos.d>0)
				poss.emplace_back(pos);
		}
	}
	if(poss.size()<=0)
		return Edge{};
	std::sort(poss.begin(), poss.end(), [](const pos_sort& a, const pos_sort& b)->bool { return a.l<b.l || (a.l==b.l && a.d<b.d); });
	*idx=poss[0].i;
	return poss[0].e;
}

void Session::finishBranch() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	Vertex v{};
	if(priv->curPos.edge) {
		if(priv->curPos.index==0) {
			v=priv->curPos.edge.leftVertex();
		} else if(priv->curPos.index==priv->curPos.edge.points().size()-1) {
			v=priv->curPos.edge.rightVertex();
		}
	}
	size_t nei;
	Edge ne{};
	if(v) {
		if(v.inLoop()) {
			if(v.parentEdge() && v.parentEdge().inLoop()) {
				ne=v.parentEdge();
				nei=0;
				if(ne.rightVertex()!=v)
					nei=ne.points().size()-1;
			} else {
				ne=getNextPosition(priv->graph, priv->curPos.point, &nei);
			}
		} else if(!v.finished()) {
			ne=getNextPosition(priv->graph, priv->curPos.point, &nei);
			auto cmd=Command::finishBranch(priv, v, ne, nei);
			priv->runCommand(cmd);
			Tracer::instance()->logMessage("STAT: ", "TRACING_FINISH");
			return;
		} else {
			ne=getNextPosition(priv->graph, priv->curPos.point, &nei);
		}
	} else {
		ne=getNextPosition(priv->graph, priv->curPos.point, &nei);
	}
	if(ne) {
		priv->jumpToPosition(ne, nei, nullptr);
		priv->updateActions();
		priv->viewer->update();
		if(priv->state==SessionState::Ready && priv->loadDataAuto)
			startDownloadCube();
	}
}
void Session::createBranch() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	if(priv->path.points.size()<2)
		return;
	if(priv->path.edge0 && priv->path.edge1) {
		Vertex v0{};
		if(priv->path.index0==0) {
			v0=priv->path.edge0.leftVertex();
		} else if(priv->path.index0==priv->path.edge0.points().size()-1) {
			v0=priv->path.edge0.rightVertex();
		}
		Vertex v1{};
		if(priv->path.index1==0) {
			v1=priv->path.edge1.leftVertex();
		} else if(priv->path.index1==priv->path.edge1.points().size()-1) {
			v1=priv->path.edge1.rightVertex();
		}
		if(v0 && v1 && v0==v1) {
			showWarning("Create branch", "Loops not allowed", this);
			return;
		} else if(!v0 && !v1 && priv->path.edge0==priv->path.edge1
				&& priv->path.index0==priv->path.index1) {
			showWarning("Create branch", "Loops not allowed", this);
			return;
		}
	}
	auto cmd=Command::createBranch(priv, priv->path);
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "TRACING_BRANCH");
}

void Session::extendBranch() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	if(priv->path.points.size()<2)
		return;
	if(priv->path.edge0 && priv->path.edge1) {
		Vertex v0{};
		if(priv->path.index0==0) {
			v0=priv->path.edge0.leftVertex();
		} else if(priv->path.index0==priv->path.edge0.points().size()-1) {
			v0=priv->path.edge0.rightVertex();
		}
		Vertex v1{};
		if(priv->path.index1==0) {
			v1=priv->path.edge1.leftVertex();
		} else if(priv->path.index1==priv->path.edge1.points().size()-1) {
			v1=priv->path.edge1.rightVertex();
		}
		if(v0 && v1 && v0==v1) {
			showWarning("Extend branch", "Loops not allowed", this);
			return;
		} else if(!v0 && !v1 && priv->path.edge0==priv->path.edge1
				&& priv->path.index0==priv->path.index1) {
			showWarning("Extend branch", "Loops not allowed", this);
			return;
		}
	}
	auto cmd=Command::extendBranch(priv, priv->path);
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "TRACING_EXTEND");
}
static inline void setMarkImp(SessionPriv* tp, int16_t m) {
	switch(tp->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	if(!tp->tgtPos.edge)
		return;
	if(tp->tgtPos.edge.points()[tp->tgtPos.index].m==m)
		return;

	auto cmd=Command::changeMark(tp, tp->tgtPos.edge, tp->tgtPos.index, static_cast<int16_t>(m));
	tp->runCommand(cmd);
}
void Session::setMark1() {
	setMarkImp(priv, 1);
	Tracer::instance()->logMessage("STAT: ", "TRACING_MARK1");
}
void Session::setMark2() {
	setMarkImp(priv, 2);
	Tracer::instance()->logMessage("STAT: ", "TRACING_MARK2");
}
void Session::setMark3() {
	setMarkImp(priv, 3);
	Tracer::instance()->logMessage("STAT: ", "TRACING_MARK3");
}
void Session::clearMark() {
	setMarkImp(priv, 0);
	Tracer::instance()->logMessage("STAT: ", "TRACING_MARK0");
}

void Session::removeEdge() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	if(!priv->curPos.edge && !priv->tgtPos.edge)
		return;
	auto cmd=Command::removeEdge(priv, priv->curPos.edge, priv->curPos.index, priv->tgtPos.edge, priv->tgtPos.index);
	priv->runCommand(cmd);
	Tracer::instance()->logMessage("STAT: ", "TRACING_DELETE");
}

void Session::abortOperation() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::Readonly:
		case SessionState::Ready:
			return;
		case SessionState::LoadingCatalog:
			priv->loadThread->cancel(); // XXX when cancelled, goto readonly
			break;
		case SessionState::Computing:
			priv->computeThread->cancel();
			Tracer::instance()->logMessage("STAT: ", "EDIT_ABORT_COMPUTE");
			break;
		case SessionState::LoadingCubes:
			priv->loadThread->cancel();
			Tracer::instance()->logMessage("STAT: ", "EDIT_ABORT_LOAD");
			//priv->setState(SessionState::Ready, "ready", false);

			break;
	}
}

void Session::saveCurrentCube() {
	if(!priv->cubes[0].data)
		return;
	QFileDialog dlg(this, "Please choose a NRRD file to save.", "", "");
	dlg.setAcceptMode(QFileDialog::AcceptSave);
	dlg.setDefaultSuffix("nrrd");
	dlg.setFileMode(QFileDialog::AnyFile);
	dlg.setNameFilter("NRRD file (*.nrrd)");
	if(dlg.exec()!=QDialog::Accepted)
		return;

	try {
		printMessage("Saving cubes...");
		OutputStream fs;
		if(!fs.open(dlg.selectedFiles()[0].toLocal8Bit().constData()))
			throwError("Cannot open file");
		fs.precision(13);
		fs.setf(std::ios::scientific);

		auto& data=priv->cubes[0].data;
		fs<<"NRRD0004\n";
		fs<<"# Created by " PACKAGE_NAME_LONG " " PACKAGE_VERSION "\n";
		switch(data.type()) {
			case CubeType::U8: fs<<"type: uint8\n"; break;
			case CubeType::I8: fs<<"type: int8\n"; break;
			case CubeType::U16: fs<<"type: uint16\n"; break;
			case CubeType::I16: fs<<"type: int16\n"; break;
			case CubeType::U32: fs<<"type: uint32\n"; break;
			case CubeType::I32: fs<<"type: int32\n"; break;
			case CubeType::U64: fs<<"type: uint64\n"; break;
			case CubeType::I64: fs<<"type: int64\n"; break;
			case CubeType::F32: fs<<"type: float\n"; break;
			case CubeType::F64: fs<<"type: double\n"; break;
			case CubeType::Unknown:
										throwError("Unknown cube type.");
		}

		fs<<"encoding: gzip\n";
		fs<<"dimension: 3\n";
		auto w=data.width();
		auto h=data.height();
		auto d=data.depth();
		fs<<"sizes: "<<w<<" "<<h<<" "<<d<<"\n";
		if(isLittleEndian()) {
			fs<<"endian: little\n";
		} else {
			fs<<"endian: big\n";
		}
		fs<<"\n";
		if(!fs)
			throwError("Failed to write header.");
		if(!fs.pushGzip())
			throwError("Failed to start gzip compressing.");

		auto ptr=data.data();
		auto ww=data.widthAdj();
		auto hh=data.heightAdj();
		auto bpv=bytes_per_voxel(data.type());
		for(int32_t z=0; z<d; z++) {
			for(int32_t y=0; y<h; y++) {
				fs.write(ptr+bpv*(y*ww+z*ww*hh), w*bpv);
			}
		}

		if(!fs.pop())
			throwError("Failed to finish gzip compressing.");
		if(!fs.close())
			throwError("Failed to close file.");
	} catch(const std::exception& e) {
		showError("Error saving cube", e.what(), this);
		return;
	}
}

void Session::takeScreenshot() {
	QFileDialog dlg(this, "Please choose a PNG file to save.", "", "");
	dlg.setAcceptMode(QFileDialog::AcceptSave);
	dlg.setDefaultSuffix("png");
	dlg.setFileMode(QFileDialog::AnyFile);
	dlg.setNameFilter("PNG files (*.png)");
	if(dlg.exec()!=QDialog::Accepted)
		return;

	auto img=priv->viewer->takeScreenshot();
	img.save(dlg.selectedFiles()[0]);
}

void Session::toggleRotate() {
	priv->viewer->toggleRotate();
}

void Session::setPositionCur(int v) {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
			return;
		case SessionState::Computing:
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	if(!priv->curPos.edge)
		return;
	if(v<0)
		return;
	if(static_cast<size_t>(v)==priv->curPos.index)
		return;
	priv->jumpToPosition(priv->curPos.edge, v, nullptr);
	priv->updateActions();
	priv->viewer->update();
	if(priv->state==SessionState::Ready && priv->loadDataAuto)
		startDownloadCube();
}
void Session::setPositionTgt(int v) {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
			return;
		case SessionState::Ready:
		case SessionState::Computing:
		case SessionState::LoadingCubes:
			break;
	}
	if(!priv->tgtPos.edge)
		return;
	if(v<0)
		return;
	if(priv->tgtPos.index==static_cast<size_t>(v))
		return;

	priv->pickPosition(priv->tgtPos.edge, v, nullptr);
	priv->viewer->update();
	priv->updateActions();
}

void Session::sourcesReady() {
	priv->sourcesReady();
}
void Session::cubeFinished() {
	auto cubes=priv->loadThread->getOutput();
	bool need_upd{false};
	bool upd_bbox{false};
	for(auto& cube: cubes) {
		if(cube.second.data) {
			if(priv->updateBoundingBox(cube.second))
				upd_bbox=true;
			for(int i=0; i<9; i++) {
				if(priv->cube_maps[i]==cube.first && !cube.second.data.cubeId().annotTree) {
					priv->cubes[i]=cube.second;
					need_upd=true;
					if(i%3==0)
						priv->computeThread->updateCube(i/3, cube.second, priv->xfuncs[i]);
					priv->viewer->updateCube(i, cube.second, priv->xfuncs[i]);
					printMessage("cube ready: ", priv->channels[cube.first], "->", i);
				}
			}
			if(priv->annot_map==cube.first && cube.second.data.cubeId().annotTree) {
				priv->updateAnnotModel(cube.second.data.cubeId().annotTree);
				priv->annotCube=cube.second;
				need_upd=true;
				//priv->viewer->updateCube(i, cube.second, priv->xfuncs[i]);
				printMessage("cube ready: ", priv->channels[cube.first], "->");
			}
		} else {
			showWarning("Failed to download source: ", priv->channels[cube.first], priv->session);
		}
	}
	if(upd_bbox)
		priv->viewer->updateBoundingBox(priv->bbox);
	if(need_upd)
		priv->viewer->update();
}
void Session::updateProgress(int cur, int tot) {
	printMessage("Progress: ", cur, "/", tot);
	priv->progr->setMaximum(tot);
	priv->progr->setValue(cur);
	if(cur==tot && priv->loadThread->reallyFinished()) {
		switch(priv->state) {
			case SessionState::Invalid:
			case SessionState::Readonly:
				return;
			case SessionState::LoadingCatalog:
				//
				// XXX put sourcesReady here
				break;
			case SessionState::Computing:
				// XXX
				break;
			case SessionState::LoadingCubes:
				priv->setState(SessionState::Ready, "ready", false);
				break;
			case SessionState::Ready:
				break;
		}
	}
}

void Session::timeoutFiveMinute() {
	if(priv->timer_check!=priv->ver) {
		Tracer::instance()->logMessage("STAT: ", "TIMEOUT");
		priv->dumpGraphDetail();
		priv->timer_check=priv->ver;
	}
}

void Session::computeFinished() {
	switch(priv->state) {
		case SessionState::Computing:
			if(priv->computeThread->getPath(&priv->path, &priv->pathConnected)) {
				double len=0;
				for(size_t i=1; i<priv->path.points.size(); i++) {
					len+=priv->path.points[i].distTo(priv->path.points[i-1]);
				}
				printMessage("Total length: ", len);
				priv->viewer->setPathData(priv->path);
			}
			priv->viewer->setProgressData({});
			priv->viewer->update();
			priv->setState(SessionState::Ready, "Finished.", false);
			break;
		default:
			break;
	}
}




void Session::showPopupMenu(const QPoint& pt) { priv->popupViewer->popup(pt); }
Options* Session::options() { return &priv->options; }
void Session::updateActions() { priv->updateActions(); }

/* misc UIs */
void Session::currentChanged(const QModelIndex& selected, const QModelIndex& deselected) {
	printMessage("current %1", priv->curRow);
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
			return;
		case SessionState::Readonly:
		case SessionState::Computing:
		case SessionState::LoadingCubes:
		case SessionState::Ready:
			break;
	}
	if(selected.isValid()) {
		if(priv->curRow!=selected.row()) {
			priv->curRow=selected.row();
			auto& p=priv->graph.trees()[priv->curRow].root().neighbors()[0];
			priv->pickPosition(p.first, p.second?p.first.points().size()-1:0, nullptr);
		}
	} else {
		priv->curRow=-1;
	}
	priv->updateActions();
	priv->viewer->update();
}
void Session::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
			return;
		case SessionState::Readonly:
		case SessionState::Computing:
		case SessionState::LoadingCubes:
		case SessionState::Ready:
			break;
	}
	auto gp=GraphPriv::get(priv->graph);
	for(auto& idx: selected.indexes()) {
		auto np=TreePriv::get(gp->trees[idx.row()]);
		np->selected=true;
	}
	for(auto& idx: deselected.indexes()) {
		auto np=TreePriv::get(gp->trees[idx.row()]);
		np->selected=false;
	}
	priv->updateActions();
	priv->viewer->update();
}
/* thread callback */
void Session::showComputeProgress() {
	auto& dat=priv->computeThread->progressData();
	priv->viewer->setProgressData(dat);
	priv->computeThread->progressDataUpdated();
	priv->viewer->update();
}
void Session::computeThreadWarning(const QString& msg) {
	showWarning("Compute thread", msg, priv->session);
}
void Session::computeThreadError(const QString& msg) {
	showError("Compute thread", msg, priv->session);
	priv->setState(SessionState::Readonly, "Readonly mode due to error in threads", false);
}
void Session::loadThreadWarning(const QString& msg) {
	showWarning("Load thread", msg, priv->session);
}
void Session::loadThreadError(const QString& msg) {
	showError("Error in load thread", msg, priv->session);
	priv->setState(SessionState::Readonly, "Readonly mode due to error in threads", false);
}
void Session::pickPosition(Edge edg, size_t idx, const Point* pos) {
	priv->pickPosition(edg, idx, pos);
}
void Session::jumpToPosition(Edge edg, size_t idx, const Point* pos) {
	priv->jumpToPosition(edg, idx, pos);
}

Session::Session(const LoadThreadInput& input):
	QMainWindow{nullptr, 0}, priv{new SessionPriv{this, input}}
{
	priv->loadOptions();
	priv->setupActions();
	priv->setupMenus();
	priv->setupSidePanel();
	priv->setupAnnotPanel();
	priv->setupStatusBar();
	priv->setupMenuBarAndToolBar();
}
Session::Session(const QString& file):
	QMainWindow{nullptr, 0}, priv{new SessionPriv{this, file}}
{
	priv->loadOptions();
	priv->setupActions();
	priv->setupMenus();
	priv->setupSidePanel();
	priv->setupAnnotPanel();
	priv->setupStatusBar();
	priv->setupMenuBarAndToolBar();
}
Session::Session(const QString& file, const QString& swap):
	QMainWindow{nullptr, 0}, priv{new SessionPriv{this, file, swap}}
{
	priv->loadOptions();
	priv->setupActions();
	priv->setupMenus();
	priv->setupSidePanel();
	priv->setupAnnotPanel();
	priv->setupStatusBar();
	priv->setupMenuBarAndToolBar();
}
Session::~Session() {
	delete priv->viewer;
	delete priv;
}


void Session::saveSession() { priv->saveSession(); }
void Session::saveSessionAs() { priv->saveSessionAs(); }
void Session::readGraph() {
	priv->readGraph(sender());
}
void Session::closeEvent(QCloseEvent* event) {
	Q_EMIT requestCloseSession(this);
	event->ignore();
}
void Session::closeSession() {
	Q_EMIT requestCloseSession(this);
}
bool Session::closeSessionImp() {
	return priv->closeSessionImp();
}

void Session::undoCommand() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::LoadingCubes:
		case SessionState::Ready:
			break;
	}
	if(priv->undoList.size()<=0)
		return;
	auto cmd=priv->undoList.front();
	priv->undoList.pop_front();
	cmd->undo(priv);
	priv->redoList.push_front(cmd); // *graph *cur *tgt
	priv->updateVersion(-1, false);
	priv->updateActions();
	priv->viewer->update();
	if(priv->state==SessionState::Ready && priv->loadDataAuto)
		startDownloadCube();
	Tracer::instance()->logMessage("STAT: ", "EDIT_UNDO");
}
void Session::redoCommand() {
	switch(priv->state) {
		case SessionState::Invalid:
		case SessionState::LoadingCatalog:
		case SessionState::Readonly:
		case SessionState::Computing:
			return;
		case SessionState::Ready:
		case SessionState::LoadingCubes:
			break;
	}
	if(priv->redoList.size()<=0)
		return;
	auto cmd=priv->redoList.front();
	priv->redoList.pop_front();
	cmd->redo(priv); // *graph *cur *tgt
	priv->undoList.push_front(cmd);
	priv->updateVersion(1, false);
	priv->updateActions();
	priv->viewer->update();
	if(priv->state==SessionState::Ready && priv->loadDataAuto)
		startDownloadCube();
	Tracer::instance()->logMessage("STAT: ", "EDIT_REDO");
}

#include "session.ui.cc"
#include "session.options.cc"
